/*
 * @Author: wangdazhuang
 * @Date: 2024-11-28 09:09:24
 * @LastEditTime: 2024-11-28 09:48:37
 * @LastEditors: wangdazhuang
 * @Description: 
 * @FilePath: /pornhub_app/lib/src/utils/m3u8_download/m3u8_downloader.dart
 */
import 'dart:convert';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_instance/src/extension_instance.dart';

import 'package:path_provider/path_provider.dart';

import '../../env/environment_service.dart';
import '../../services/storage_service.dart';
import '../enum.dart';
import '../logger.dart';
import 'm3u8_parser.dart';
import 'm3u8_utils.dart';

const _dioExtraKeyTakeAuth = '__auth__';

typedef DurationProgressCallback = void Function(
    double downloaded, double total);

void _d(String m) {
  if (Environment.enableM3u8DownloadLog) {
    logger.d('[M3u8Downloader] $m');
  }
}

class M3u8Downloader {
  final int videoId;
  final Uri m3u8Uri;
  final int tryMax;
  final DurationProgressCallback? onProgress;
  final ValueCallback<String>? onDone;
  final ValueCallback<String>? onError;
  final VoidCallback? onCancel;

  bool _isCanceled = false;
  CancelToken? _cancelToken;
  final _dio = Dio(BaseOptions(
    connectTimeout: const Duration(minutes: 1),
  ));

  M3u8Downloader(
    this.m3u8Uri, {
    required this.videoId,
    this.tryMax = 5,
    this.onProgress,
    this.onDone,
    this.onError,
    this.onCancel,
  }) {
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        if (options.extra[_dioExtraKeyTakeAuth] == true) {
          final storageService = Get.find<StorageService>();
          if (storageService.token != null) {
            options.headers["aut"] = storageService.token;
          }
          final t = DateTime.now().millisecondsSinceEpoch.toString();
          options.headers["t"] = t;
          options.headers["s"] =
              md5.convert(utf8.encode(t.substring(3, 8))).toString();
          options.headers["User-Mark"] = Environment.appName;
          options.headers["deviceId"] = storageService.deviceId;
        }
        return handler.next(options);
      },
    ));
  }

  Future<String?> getSaveDir() async {
    try {
      final dir = await getApplicationDocumentsDirectory();
      return '${dir.path}/downloads/video/$videoId';
    } catch (e) {
      return null;
    }
  }

  void start() {
    _cancelToken = CancelToken();
    _downloadAll();
  }

  void cancel() {
    if (_isCanceled) return;
    _isCanceled = true;
    _cancelToken?.cancel();
  }

  Future<bool> _download(
    Uri url,
    String savePath, {
    ProgressCallback? onProgress,
    CancelToken? cancelToken,
    int tryMax = 3,
    required bool takeAuth,
  }) async {
    for (int tried = 0; tried < tryMax && !_isCanceled; tried++) {
      final tmpSavePath = '$savePath.tmp';
      Response resp;
      try {
        resp = await _dio.downloadUri(
          url,
          tmpSavePath,
          onReceiveProgress: onProgress,
          cancelToken: cancelToken,
          deleteOnError: true,
          options:
              takeAuth ? Options(extra: {_dioExtraKeyTakeAuth: true}) : null,
        );
      } catch (e) {
        if (e is DioException && CancelToken.isCancel(e)) {
          _d('下载取消,$url, e:$e');
          return false;
        }
        _d('下载发生错误 $url, e:$e');
        continue;
      }
      // 2xx
      if (resp.statusCode != null &&
          resp.statusCode! >= HttpStatus.ok &&
          resp.statusCode! < HttpStatus.multipleChoices) {
        final fileLength = await M3u8Utils.fileLength(tmpSavePath);
        if (fileLength == null || fileLength == 0) {
          _d('下载完成, code为200,但是没有长度, length:$fileLength, url:$url, savePath:$savePath');
          return false;
        }
        if (await M3u8Utils.renameFile(tmpSavePath, savePath)) {
          return true;
        }
        await M3u8Utils.deleteFile(tmpSavePath);
      }
    }
    return false;
  }

  Future<void> _downloadAll() async {
    final saveDir = await getSaveDir();
    if (saveDir == null) {
      onError?.call('数据目录发生错误');
      return;
    }
    // 创建下载目录
    if (!await M3u8Utils.ensureDir(saveDir)) {
      onError?.call('数据目录发生错误');
      return;
    }
    if (_isCanceled) {
      onCancel?.call();
      return;
    }
    // 下载m3u8文件
    final rawM3u8SavePath = '$saveDir/index.raw.m3u8';
    final rawM3u8Exists = await M3u8Utils.fileExists(rawM3u8SavePath);
    if (rawM3u8Exists == null) {
      onError?.call('文件发生错误');
      return;
    }
    if (rawM3u8Exists == false) {
      _d('开始下载 m3u8文件..$m3u8Uri, $rawM3u8SavePath');
      if (!await _download(m3u8Uri, rawM3u8SavePath, takeAuth: true)) {
        onError?.call('下载发生错误(1)');
        return;
      }
    } else {
      _d('原始m3u8文件已存在,skip, videoId:$videoId, path:$rawM3u8SavePath');
    }

    // 解析m3u8文件
    _d('开始解析 m3u8文件..$rawM3u8SavePath');
    final content = await M3u8Utils.readFileAsString(rawM3u8SavePath);
    if (content == null) {
      _d('解析 m3u8文件失败..$rawM3u8SavePath');
      onError?.call('解析m3u8发生错误(1)');
      return;
    }
    if (_isCanceled) {
      onCancel?.call();
      return;
    }
    final m3u8 = M3u8File.parse(content);
    if (m3u8 == null) {
      _d('解析 m3u8文件失败..$rawM3u8SavePath');
      onError?.call('解析m3u8发生错误(2)');
      return;
    }
    _d('解析m3u8成功 $videoId, duration:${m3u8.duration}');

    // 下载key文件
    M3u8Encryption? newEncryption;
    if (m3u8.encryption != null) {
      final fullUri = m3u8.encryption!.fullUri(m3u8Uri);
      if (fullUri == null) {
        _d('获取key文件地址失败..videoId:$videoId keyUrl:${m3u8.encryption?.uri}');
        onError?.call('解析m3u8发生错误(3)');
        return;
      }
      const name = 'enc.key';
      final keySavePath = '$saveDir/$name';
      final keyExists = await M3u8Utils.fileExists(keySavePath);
      if (keyExists == null) {
        onError?.call('文件发生错误');
        return;
      }
      if (keyExists == false) {
        _d('开始下载key文件,videoId:$videoId, url:$fullUri');
        if (!await _download(fullUri, keySavePath, takeAuth: true)) {
          _d('下载key文件失败..videoId:$videoId, raw:$fullUri');
          onError?.call('下载发生错误(2)');
          return;
        } else {
          _d('下载key文件成功, videoId:$videoId, path:$keySavePath');
        }
      } else {
        _d('key文件已存在,skip, videoId:$videoId, path:$keySavePath');
      }
      newEncryption = M3u8Encryption(
        method: m3u8.encryption!.method,
        uri: name,
      );
    }

    final localSegments = <M3u8Segemnt>[];
    // 逐个下载ts文件
    _d('开始下载ts文件,videoId:$videoId, ts数:${m3u8.segments.length}, 时长${m3u8.duration}');
    double downloadedDuration = .0;
    for (final seg in m3u8.segments) {
      final fullUri = seg.fullUri(m3u8Uri);
      _d('开始下载ts文件, videoId:$videoId, ts:$fullUri');
      if (fullUri == null) {
        onError?.call('m3u8错误');
        return;
      }
      final tsName = fullUri.pathSegments.last;
      final tsSavePath = '$saveDir/$tsName';
      final fileExists = await M3u8Utils.fileExists(tsSavePath);
      if (fileExists == true) {
        downloadedDuration += seg.duration;
        onProgress?.call(downloadedDuration, m3u8.duration);
      } else {
        if (!await _download(
          fullUri,
          tsSavePath,
          takeAuth: false,
          onProgress: (count, total) {
            if (total <= 0) {
              //?
            } else {
              final rate = count / total; // 当前ts大小的比例
              final rateDuration = rate * seg.duration; // 换算到时间
              final tmpDownloadedDuration = downloadedDuration + rateDuration;
              onProgress?.call(tmpDownloadedDuration, m3u8.duration);
              _d('download Progress videoId:$videoId, $tmpDownloadedDuration/${m3u8.duration}=${tmpDownloadedDuration / m3u8.duration}');
            }
          },
        )) {
          onError?.call('下载发生错误(3)');
          _d('下载ts文件失败.., fullUr:$fullUri');
          return;
        }
        downloadedDuration += seg.duration;
        onProgress?.call(downloadedDuration, m3u8.duration);
      }
      localSegments.add(M3u8Segemnt(seg.inf, seg.duration, tsName));
    }
    // 写入本地的m3u8文件
    final localM3u8 = M3u8File(
      segments: localSegments,
      encryption: newEncryption,
      extXLines: m3u8.extXLines,
    );
    final newM3u8SavePath = '$saveDir/index.m3u8';
    if (!await M3u8Utils.writeStringToFile(
        newM3u8SavePath, localM3u8.toString())) {
      onError?.call('写入文件发生错误');
      _d('写入新的m3u8文件失败.., $newM3u8SavePath');
      return;
    }
    _d('下载完成., $m3u8Uri==>$saveDir');
    onDone?.call(newM3u8SavePath);
    return;
  }
}
